using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
#if TEST
using Xunit;
#endif

namespace Fadd.Commands.Net
{
    /// <summary>
    /// This class creates a command channel which can be used to send a command to a remote end point.
    /// </summary>
    public class CommandChannel : IDisposable
    {
        /// <summary>
        /// Try to use this port if it's not occupied.
        /// </summary>
        public const int DefaultPort = 9813;

        private readonly ICommandDispatcher _dispatcher;
        //private readonly ObjectCopier _copier = new ObjectCopier();
        private const int CommandTimeout = 10000;
        private int _lastSequenceNumber;
        private readonly Dictionary<int, CommandMapping> _packets = new Dictionary<int, CommandMapping>();
        private BinaryChannel _channel;

        /// <summary>
        /// Invoked when client disconnects
        /// </summary>
        public event DisconnectedHandler Disconnected = delegate{};

        #region CommandMapping
        private class CommandMapping
        {
            private readonly int _sequenceNumber;
            private readonly ManualResetEvent _event;
            private Command _command;
            public CommandMapping(int sequenceNumber)
            {
                _event = new ManualResetEvent(false);
                _sequenceNumber = sequenceNumber;
            }

            public int SequenceNumber
            {
                get { return _sequenceNumber; }
            }

            public Command Command
            {
                get { return _command; }
                set { _command = value; }
            }

            public bool Wait()
            {
                return _event.WaitOne(CommandTimeout, true);
            }

            public void Signal()
            {
                _event.Set();
            }
        }
        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="CommandChannel"/> class.
        /// </summary>
        /// <param name="dispatcher">Dispatcher used to invoke commands comming through the channel.</param>
        public CommandChannel(ICommandDispatcher dispatcher)
        {
            Check.Require(dispatcher, "dispatcher");
            _dispatcher = dispatcher;
        }

        private bool OnRemoteCommand(object source, CommandEventArgs args)
        {
            if (!(args.Command is IRemote))
                return false;

            CommandMapping mapping = CreateMapping();
            CommandPacket packet = new CommandPacket(mapping.SequenceNumber, args.Command);
            _channel.Send(packet);
            if (mapping.Wait())
                ((IRemote)args.Command).CopyReply(mapping.Command);
//                _copier.Copy(mapping.Command, args.Command);

            _packets.Remove(mapping.SequenceNumber);

            //todo: Log timeout
            return true;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="CommandChannel"/> class.
        /// </summary>
        /// <param name="dispatcher">Dispatcher used to invoke commands comming through the channel.</param>
        /// <param name="socket">Socket used to transport commands.</param>
        public CommandChannel(ICommandDispatcher dispatcher, Socket socket)
            : this(dispatcher, new BinaryChannel(socket))
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="CommandChannel"/> class.
        /// </summary>
        /// <param name="dispatcher">Dispatcher used to invoke commands comming through the channel.</param>
        /// <param name="channel">Channel used to tranport commands.</param>
        public CommandChannel(ICommandDispatcher dispatcher, BinaryChannel channel)
        {
            Check.Require(channel, "channel");
            Check.Require(dispatcher, "dispatcher");

            _channel = channel;
            _channel.ObjectReceived += OnObjectReceived;
            _channel.Disconnected += OnDisconnected;
            _dispatcher = dispatcher;
        }

        /// <summary>
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="CommandChannel"/> is reclaimed by garbage collection.
        /// </summary>
        ~CommandChannel()
        {
            Dispose();
        }

        /// <summary>
        /// Send unhandled commands through the tunnel.
        /// </summary>
        /// <remarks>
        /// Off by defualt.
        /// </remarks>
        /// <param name="value"></param>
        public void ProcessUnhandledCommands(bool value)
        {
            if (value)
                _dispatcher.Unhandled += OnRemoteCommand;
            else
                _dispatcher.Unhandled -= OnRemoteCommand;
        }

        /// <summary>
        /// Handle commands tagged with the <see cref="IRemote"/> interface.
        /// </summary>
        /// <param name="value"></param>
        /// <remarks>Off by default.</remarks>
        public void ProcessRemoteCommands(bool value)
        {
            if (value)
                _dispatcher.Add(typeof(IRemote), OnRemoteCommand);
            else
                _dispatcher.Remove(typeof(IRemote), OnRemoteCommand);
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <filterpriority>2</filterpriority>
        public void Dispose()
        {
            if (_channel != null)
            {
                _channel.Dispose();
                _channel = null;
            }
            Disconnected = null;
            //_copier.Dispose();
        }

        private void OnDisconnected(object source, DisconnectedEventArgs args)
        {
            Disconnected(this, args);
        }

        /// <summary>
        /// True if socket should auto reconnect connection is closed due to any network error.
        /// </summary>
        public bool AutoReconnect
        {
            set { _channel.ShouldReconnect = value; }
        }

        /// <summary>
        /// Connect to other side.
        /// </summary>
        /// <param name="endPoint"></param>
        /// <exception cref="InvalidOperationException"></exception>
        public void Open(IPEndPoint endPoint)
        {
            try
            {
                if (_channel == null)
                {
                    _channel = new BinaryChannel();
                    _channel.ObjectReceived += OnObjectReceived;
                    _channel.Disconnected += OnDisconnected;
                }
                _channel.Open(endPoint);
            }
            catch (SocketException err)
            {
                throw new InvalidOperationException("Failed to open channel.", err);
            }
        }

        /// <summary>
        /// Close command tunnel
        /// </summary>
        public void Close()
        {
            _channel.ObjectReceived -= OnObjectReceived;
            _channel.Disconnected -= OnDisconnected;
            _channel.Close();
        }

        private void OnObjectReceived(object source, ObjectReceivedEventArgs args)
        {
            CommandPacket packet = (CommandPacket) args.Object;
            if (packet.Reply)
            {
                lock (_packets)
                {
                    if (_packets.ContainsKey(packet.SequenceNumber))
                    {
                        CommandMapping mapping = _packets[packet.SequenceNumber];
                        mapping.Command = packet.Command;
                        _packets.Remove(packet.SequenceNumber);
                        mapping.Signal();
                    }
                }
            }
            else
            {
                _dispatcher.Invoke(source, packet.Command, OnRemoteCommand);
                packet.Reply = true;
                _channel.Send(packet);
            }
        }


        /// <summary>
        /// Stops this instance.
        /// </summary>
        public void Stop()
        {
            _channel.Close();
        }

        internal static void GetClientServer(out BinaryChannel server, out BinaryChannel client)
        {
            client = new BinaryChannel();
            client.ShouldReconnect = false;
            int port = 8291;
            TcpListener listener = new TcpListener(IPAddress.Any, port);
            listener.Start();
            IAsyncResult res = listener.BeginAcceptSocket(null, null);

            client.Open(new IPEndPoint(IPAddress.Loopback, port));
            Socket socket = listener.EndAcceptSocket(res);
            listener.Stop();
            server = new BinaryChannel(socket);
        }

        #region Tests
#if TEST

        [Fact]
        private static void TestChannels()
        {
            BinaryChannel server, client;
            GetClientServer(out server, out client);

            ManualResetEvent _wait = new ManualResetEvent(false);

            server.ObjectReceived += delegate(object source, ObjectReceivedEventArgs args)
                                         {
                                             Assert.IsType<TestCommand>(args.Object);
                                             TestCommand cmd = (TestCommand) args.Object;
                                             Assert.Equal("jonas", cmd.UserName);
                                             cmd.MyList.Add("Hello world!");
                                             server.Send(cmd);
                                         };

            client.ObjectReceived += delegate(object source, ObjectReceivedEventArgs args)
                                         {
                                             Assert.IsType<TestCommand>(args.Object);
                                             TestCommand cmd = (TestCommand) args.Object;
                                             Assert.Equal("jonas", cmd.UserName);
                                             Assert.Equal(1, cmd.MyList.Count);
                                             Assert.Equal("Hello world!", cmd.MyList[0]);
                                             _wait.Set();
                                         };

            client.Send(new TestCommand("jonas"));
            Assert.True(_wait.WaitOne(30000, false));
        }

        [Fact]
        private static void TestTunnels()
        {
            BinaryChannel serverChannel, clientChannel;
            GetClientServer(out serverChannel, out clientChannel);

            // setup server side
            CommandManager serverMgr = new CommandManager();
            serverMgr.Add(typeof(TestCommand), OnTestTunnelCommand);
            CommandChannel server = new CommandChannel(serverMgr, serverChannel);


            // setup client side.
            CommandManager clientMgr = new CommandManager();
            CommandChannel client = new CommandChannel(clientMgr, clientChannel);
            client.ProcessUnhandledCommands(true);

            // invoke command
            TestCommand cmd = new TestCommand("arne");
            clientMgr.Invoke(cmd);

            // validate that the server fixed it.
            Assert.Equal(1, cmd.MyList.Count);
            Assert.Equal("Hello World!", cmd.MyList[0]);
        }

        private static bool OnTestTunnelCommand(object source, CommandEventArgs args)
        {
            TestCommand cmd = (TestCommand) args.Command;
            Assert.Equal("arne", cmd.UserName);
            cmd.MyList.Add("Hello World!");
            return true;
        }
#endif
        #endregion
        private CommandMapping CreateMapping()
        {
            CommandMapping mapping = new CommandMapping(++_lastSequenceNumber);
            if (_lastSequenceNumber >= 65535)
                _lastSequenceNumber = 0;
            _packets.Add(mapping.SequenceNumber, mapping);
            return mapping;
        }
    }
}
